/*
 * Copyright 2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package groovyx.gaelyk.obgaektify

import com.googlecode.objectify.ObjectifyFactory
import com.googlecode.objectify.ObjectifyService
import com.googlecode.objectify.Objectify
import com.googlecode.objectify.Key

/**
 * Provides methods that give easier access to the com.googlecode.objectify packages
 *
 * @author Jeremy Brown
 */
class ObgaektifyCategory
{
	
	//========================================================
	//Methods that use a Key
	//========================================================
	//Get a single Key
	static Obgaektifiable fetch(Key key)
	{
		return (new ObgaektifyDAO()).find(key)
	}
	
	static List<Obgaektifiable> fetchList(Iterable keys)
	{
		def list = []
		(new ObgaektifyDAO()).get(keys).each{key, val-> list << val}
		return list
	}
	
	static Map<Key,Obgaektifiable> fetch(Iterable keys)
	{
		return (new ObgaektifyDAO()).get(keys)
	}
	
	static void destroy(Key key)
	{
		(new ObgaektifyDAO()).delete(key)
	}
	
	static getNameOrId(Key key)
	{
		return (key.getName() ?: key.getId())
	}
	
	static String asType(Key key, Class clazz)
	{
		if(clazz == String)
		{
			return ObjectifyService.factory().keyToString(key)
		}
	}
	
	static String keyToString(Key key)
	{
		return ObjectifyService.factory().keyToString(key)
	}


	//========================================================
	//Methods that use an Obgaektifiable domain object
	//========================================================
	
	//e.g. myDomainObject.store()
	static Key store(Obgaektifiable obj)
	{
		return (new ObgaektifyDAO()).put(obj)
	}
	
	static Map<Key,Obgaektifiable> store(Iterable<Obgaektifiable> list)
	{
		return (new ObgaektifyDAO()).put(list)
	}
	
	//e.g. myDomainObject.destroy()
	static void destroy(Obgaektifiable obj)
	{
		(new ObgaektifyDAO()).delete(obj)
	}
	
	static void destroy(Iterable listOfKeysOrObgaektifiables)
	{
		(new ObgaektifyDAO()).delete(listOfKeysOrObgaektifiables)
	}
	
	static Object getNameOrId(Obgaektifiable obj)
	{
		return obj.id
	}
	
	static Key getKey(Obgaektifiable obj)
	{
		return ObjectifyService.factory().getKey(obj)
	}

	static String getKeyString(Obgaektifiable obj)
	{
		ObjectifyFactory factory = ObjectifyService.factory()
		return factory.keyToString(factory.getKey(obj))
	}
	
	//========================================================
	//Methods that use a String representation of a Key
	//========================================================
	
	//Takes a web-safe String represenation of a Key and returns a Key
	static Key stringToKey(String keyString)
	{
		def key = null
		try
		{
			key = ObjectifyService.factory().stringToKey(keyString)
		}
		catch(IllegalArgumentException){}
		return key
	}
	
	//Takes a web-safe String representation of a Key and
	//retrieves the object from the datastore for that Key
	static Obgaektifiable fetch(String keyString)
	{
		Key key = ObgaektifyCategory.stringToKey(keyString)
		return ObgaektifyCategory.fetch(key)
	}
	
	//========================================================
	//Methods that use an Obgaektifiable domain class + name or id
	//========================================================

	//Takes an Id that may have been passed in as a String and changes it to a Long if
	//the Obgaektifiable class it goes with uses a Long as Id (i.e. ObgaektifiableLongId)
	static Object resolveId(Class<Obgaektifiable> c, String id)
	{
		if(c.idType == Long)
		{
			return (id as Long)
		}
		else
		{
			return (id as String)
		}
	}
	
	static Object resolveIdList(Class<Obgaektifiable> c, Iterable idList)
	{
		if(c.idType == Long)
		{
			idList = idList.collect{it as Long}
		}
		else
		{
			idList = idList.collect{it as String}
		}
		return idList
	}
	
	//e.g. MyDomainClass.delete(myDomainObjectId)
	static void destroy(Class<Obgaektifiable> c, String id)
	{
		(new ObgaektifyDAO()).delete(c, resolveId(c, id))
	}
	
	//e.g. MyDomainClass.delete(myDomainObjectId)
	static void destroy(Class<Obgaektifiable> c, Long id)
	{
		(new ObgaektifyDAO()).delete(c, id as long)
	}	

	//Delete objects based on a class and a list of ids
	//e.g. MyDomainClass.delete([id1, id2, id3, id4])
	static void destroy(Class<Obgaektifiable> c, Iterable idList)
	{
		def keyList = resolveIdList(c, idList).collect{new Key(c, it)}
		(new ObgaektifyDAO()).delete(keyList)
	}
	
	static Obgaektifiable fetch(Class<Obgaektifiable> c, String id)
	{
		return (new ObgaektifyDAO()).get(c, resolveId(c, id))
	}
	
	static Obgaektifiable fetch(Class<Obgaektifiable> c, Long id)
	{
		return (new ObgaektifyDAO()).get(c, id as long)
	}
	
	//e.g. MyDomainClass.getList(listOfMyDomainObjectIds)
	static List<Obgaektifiable> fetchList(Class<Obgaektifiable> c, Iterable idList)
	{
		def list = []
		idList = resolveIdList(c, idList)
		def map = (new ObgaektifyDAO()).get(c, idList)
		map.each{key, val-> list << val}
		return list
	}
	
	//e.g. MyDomainClass.getMap(listOfMyDomainObjectIds)
	static Map<Key,Obgaektifiable> fetch(Class<Obgaektifiable> c, Iterable idList)
	{
		return (new ObgaektifyDAO()).get(c, resolveIdList(c, idList))
	}
	
	
	//========================================================
	//Methods that use an Objectify object with arguments
	//These methods would be useful when trying to use an
	//existing Objectify object, such as during a transaction.
	//========================================================
	
	//e.g. objectify.store(myDomainObject)
	static Key store(Objectify ofy, Obgaektifiable obj)
	{
		ofy.put(obj)
	}
	
	//e.g. objectify.store([myDomainObject1, myDomainObject2, myDomainObject3])
	static Map<Key,Obgaektifiable> store(Objectify ofy, Iterable<Obgaektifiable> objList)
	{
		ofy.put(objList)
	}

	//e.g. objectify.getList([key1, key2, key3])
	//This returns a list of objects.
	//Calling objectify.get([key1, key2, key3]) is the normal Objectify method which returns a Map of Key:Object
	static List<Obgaektifiable> fetchList(Objectify ofy, Iterable<Key> idList)
	{
		def list = []
		def map = ofy.get(idList)
		map.each{key, val-> list << val}
		return list
	}
	
	//e.g. MyDomainClass.delete(myDomainObjectId)
	static void destroy(Objectify ofy, Class<Obgaektifiable> c, String id)
	{
		ofy.delete(c, resolveId(c, id))
	}
	
	//e.g. MyDomainClass.delete(myDomainObjectId)
	static void destroy(Objectify ofy, Class<Obgaektifiable> c, Long id)
	{
		ofy.delete(c, id as long)
	}
	
	static Obgaektifiable fetch(Objectify ofy, Class<Obgaektifiable> c, String id)
	{
		return ofy.get(c, resolveId(c, id))
	}
	
	static Obgaektifiable fetch(Objectify ofy, Class<Obgaektifiable> c, Long id)
	{
		return ofy.get(c, id as long)
	}


	//========================================================
	//Search methods
	//========================================================
	/*
		These three different forms of the search method need to be defined in order to support:
		1. MyDomainClass.search() //queries all objects of type MyDomainClass
		2. MyDomainClass.search(optionMap) //queries objects of type MyDomainClass with options in a map
		3. objectify.search(myDomainClass) //queries objects of type MyDomainClass, using an already instantiated Objectify object
		4. objectify.search(myDomainClass, options) //queries objects of type MyDomainClass with options in a map, and using an already instantiated Objectify object
	*/
	static List<Obgaektifiable> search(Class<Obgaektifiable> c, Map options = [:])
	{
		return search((new ObgaektifyDAO()).objectify, c, options)
	}
	
	static List<Obgaektifiable> search(Objectify ofy, Class<Obgaektifiable> c, Map options = [:])
	{
		//Options:
		//filter/select
		//order/sort
		//limit
		//offset
		//cursor
		//ancestor
		//keysOnly
		
		//Valid filter/select operators:
		//=
		//==
		//>
		//>=
		//<
		//<=
		//!=
		//<>
		//in
		
		def query = ofy.query(c)
		
		/*
			Use the same format as Objectify for filters (except for in).
			Spaces between the property name and the operator can be omitted.
			For the "in" operator, use a list instead, with the first list element being the property name.
			e.g. The following filter would search for businessType matching various food service categories in Whittier:
			filter: [
						"city==":"Whittier",
						"businessType in":["Cafe", "Delicatessen", "Dive", "Fast Food", "Hot Dog Stand", "Restaurant", "Snack Bar"]
					]
		*/
		if(options.select){options.filter = options.select}
		if(options.filter)
		{
			def propertyAndOperator
			options.filter.each{filter, value->
				if(value instanceof List)
				{
					propertyAndOperator = filter.trim()
					if(!propertyAndOperator.endsWith(" in")){propertyAndOperator += " in"}
					query = query.filter(propertyAndOperator, value)
				}
				else if(filter =~ /.*\^=\s*$/)
				{
					//Match at the start of the string by using a combination of >= and <
					def value1 = value
					def value2 = ++value
					def prop = filter.replaceAll(/\^=\s*$/, "")
					query = query.filter(prop + " >=", value1)
					query = query.filter(prop + " <", value2)
				}
				else
				{
					propertyAndOperator = filter.replaceAll(/^([^!=><\s]+)\s*([!=><]+).*/, "\$1 \$2") //Put a space between the data name and the operator: e.g. "city==" becomes "city =="
					query = query.filter(propertyAndOperator, value)
				}
			}
		}
		
		/*
			Sort/order
			Use either the sort: or order: keyword and specify a list of sort criteria (i.e. property names)
			By default, the sort order is ASCENDING
			Prepend a "-" to indicate DESCENDING sort order.
			e.g. The following sort would sort words with highest frequency at the top of the list (DESCENDING), but sort alphabetically for words with the same wordFrequency (ASCENDING)
			sort: ["-wordFrequency", "word"]
		*/
		if(options.sort){options.order = options.sort}
		if(options.order)
		{
			options.order.each{order->
				query = query.order(order)
			}
		}
		
		if(options.limit){query = query.limit(options.limit)}
		if(options.offset){query = query.offset(options.offset)}
		if(options.cursor){query = query.cursor(options.cursor)}
		if(options.ancestor){query = query.ancestor(options.ancestor)}

		if(options.keysOnly)
		{
			return query.listKeys()
		}
		else
		{
			return query.list()
		}
	}
	
	
	static List<Key> searchKeys(Class<Obgaektifiable> c, Map options = [:])
	{
		return searchKeys((new ObgaektifyDAO()).objectify, c, options)
	}
	
	static List<Key> searchKeys(Objectify ofy, Class<Obgaektifiable> c, Map options = [:])
	{
		def newOptions = [:]
		newOptions.putAll(options)
		newOptions.keysOnly = "true"
		return search(ofy, c, newOptions)
	}
}